/*
* Copyright 2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.grails.maven.plugin;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.*;
import org.apache.maven.settings.Proxy;
import org.apache.maven.settings.Settings;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.graph.DependencyFilter;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.resolution.ArtifactResult;
import org.eclipse.aether.util.filter.AndDependencyFilter;
import org.eclipse.aether.util.filter.ExclusionsDependencyFilter;
import org.eclipse.aether.util.filter.ScopeDependencyFilter;
import org.grails.launcher.GrailsLauncher;
import org.grails.launcher.RootLoader;
import org.grails.maven.plugin.tools.AbstractGrailsRuntime;
import org.grails.maven.plugin.tools.DefaultGrailsRuntime;
import org.grails.maven.plugin.tools.ForkedGrailsRuntime;
import org.grails.maven.plugin.tools.GrailsServices;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
/**
* Common services for all Mojos using Grails
*
* @author <a href="mailto:aheritier@gmail.com">Arnaud HERITIER</a>
* @author Peter Ledbrook
* @author Jonathan Pearlin
* @author Graeme Rocher
*
* @version $Id$
*/
public abstract class AbstractGrailsMojo extends AbstractMojo {
public static final String DEPENDENCY_FILE_LOC = "org.grails.ide.eclipse.dependencies.filename";
public static final String GRAILS_BUILD_LISTENERS = "grails.build.listeners";
public static final String PLUGIN_PREFIX = "grails-";
public static final String APP_GRAILS_VERSION = "app.grails.version";
public static final String APP_VERSION = "app.version";
public static final String APP_NAME = "app.name";
public static final String SPRING_LOADED_VERSION = "1.2.4.RELEASE";
public static final List<String> COMPILE_PLUS_RUNTIME_SCOPE = Arrays.asList("compile", "runtime");
/**
* Whether to activate the reloading agent (forked mode only) for this command
*
* @parameter expression="${activateAgent}"
*/
protected boolean activateAgent;
/**
* The directory where is launched the mvn command.
*
* @parameter default-value="${basedir}"
* @required
*/
protected File basedir;
/**
* The Grails environment to use.
*
* @parameter expression="${grails.env}"
*/
protected String env;
/**
* The Grails environment to use.
*
* @parameter expression="${environment}"
*/
protected String grailsEnv;
/**
* The Grails environment to use.
*
* @parameter expression="${grailsVersion}"
*/
protected String grailsVersion;
/**
* The version of Groovy used
*/
private String groovyVersion;
/**
* The Grails work directory to use.
*
* @parameter expression="${grails.grailsWorkDir}" default-value="${project.build.directory}/work"
*/
protected String grailsWorkDir;
/**
* Whether to run Grails in non-interactive mode or not. The default
* is to run interactively, just like the Grails command-line.
*
* @parameter expression="${nonInteractive}" default-value="true"
* @required
*/
protected boolean nonInteractive = true;
/**
* Turns on/off stacktraces in the console output for Grails commands.
*
* @parameter expression="${showStacktrace}" default-value="false"
*/
protected boolean showStacktrace;
/**
* Whether the JVM is forked for executing Grails commands
*
* @parameter expression="${fork}" default-value="false"
*/
protected boolean fork = false;
/**
* List of arguments passed to the forked VM
*
* @parameter
*/
protected List forkedVmArgs;
/**
* Whether the JVM is forked for executing Grails commands
*
* @parameter expression="${forkDebug}" default-value="false"
*/
protected boolean forkDebug = false;
/**
* Whether the JVM is forked for executing Grails commands
*
* @parameter expression="${forkPermGen}" default-value="256"
*/
protected int forkPermGen = 256;
/**
* Whether the JVM is forked for executing Grails commands
*
* @parameter expression="${forkMaxMemory}" default-value="1024"
*/
protected int forkMaxMemory = 1024;
/**
* Whether the JVM is forked for executing Grails commands
*
* @parameter expression="${forkMinMemory}" default-value="512"
*/
protected int forkMinMemory = 512;
/**
* The directory where plugins are stored.
*
* @parameter expression="${pluginsDirectory}" default-value="${basedir}/plugins"
* @required
*/
protected File pluginsDir;
/**
* The Maven settings reference.
*
* @parameter expression="${settings}"
* @required
* @readonly
*/
protected Settings settings;
/**
* POM
*
* @parameter expression="${project}"
* @readonly
* @required
*/
protected MavenProject project;
/**
* The current repository/network configuration of Maven.
*
* @parameter expression="${localRepository}"
* @required
* @readonly
*/
private ArtifactRepository localRepository;
/**
* Extra classpath entries as a comma separated list of file names.
* For entries with a comma in their name, use backslash to escape.
*
* INTERNAL This parameter is not meant to be used externally. It is used by IDEs
* that require extra classpath entries to execute grails commands.
*
* @parameter
*/
private String extraClasspathEntries;
/**
* Fully qualified classname of a grails build listener to attach
* to the Grails command
*
* @parameter
*/
private String grailsBuildListener;
/**
* Fully qualified path name of the project dependency file to create.
*
* INTERNAL This parameter is not meant to be used externally. This parameter is used by
* IDEs to generate project dependency information during builds.
*/
private String dependencyFileLocation;
/**
* @component
* @readonly
*/
private GrailsServices grailsServices;
/**
* Utility for resolving dependencies from Maven
*
* @component
*/
private ProjectDependenciesResolver projectDependenciesResolver;
/**
* The entry point to Aether, i.e. the component doing all the work.
*
* @component
*/
private RepositorySystem repoSystem;
/**
* For building metadata about projects
*
* @component
*/
private ProjectBuilder projectBuilder;
/**
* The current repository/network configuration of Maven.
*
* @parameter default-value="${repositorySystemSession}"
* @readonly
*/
private RepositorySystemSession repoSession;
/**
* The project's remote repositories to use for the resolution of plugins and their dependencies.
*
* @parameter default-value="${project.remoteProjectRepositories}"
* @readonly
*/
private List<RemoteRepository> remoteRepos;
protected AbstractGrailsMojo() {
}
/**
* Returns the configured base directory for this execution of the plugin.
*
* @return The base directory.
*/
protected File getBasedir() {
if (basedir == null) {
throw new RuntimeException("Your subclass have a field called 'basedir'. Remove it and use getBasedir() " +
"instead.");
}
return this.basedir;
}
protected String getEnvironment() {
if(env == null) {
return grailsEnv;
}
return env;
}
/**
* Returns the {@code GrailsServices} instance used by the plugin with the base directory
* of the services object set to the configured base directory.
*
* @return The underlying {@code GrailsServices} instance.
*/
protected GrailsServices getGrailsServices() {
grailsServices.setBasedir(basedir);
return grailsServices;
}
/**
* Executes the requested Grails target. The "targetName" must match a known
* Grails script provided by grails-scripts.
*
* @param targetName The name of the Grails target to execute.
* @throws MojoExecutionException if an error occurs while attempting to execute the target.
*/
protected void runGrails(final String targetName) throws MojoExecutionException {
runGrails(targetName, System.getProperty("grails.cli.args"));
}
/**
* Executes the requested Grails target. The "targetName" must match a known
* Grails script provided by grails-scripts.
*
* @param targetName The name of the Grails target to execute.
* @param args String of arguments to be passed to the executed Grails target.
* @throws MojoExecutionException if an error occurs while attempting to execute the target.
*/
protected void runGrails(final String targetName, String args) throws MojoExecutionException {
configureMavenProxy();
handleVersionSync();
if(fork) {
ForkedGrailsRuntime fgr = new ForkedGrailsRuntime(createExecutionContext(targetName, args));
if (activateAgent) {
File springLoadedJar = resolveArtifact("org.springframework:springloaded:" + SPRING_LOADED_VERSION);
if (springLoadedJar != null) {
fgr.setReloadingAgent(springLoadedJar);
} else {
getLog().warn("Grails Reloading: org.springframework:springloaded:" + SPRING_LOADED_VERSION + " not found");
getLog().error("Grails Reloading: not enabled");
}
}
fgr.setDebug(forkDebug);
fgr.setMaxMemory(forkMaxMemory);
fgr.setMaxPerm(forkPermGen);
fgr.setMinMemory(forkMinMemory);
try {
fgr.run();
} catch (Exception e) {
throw new RuntimeException("Error forking vm: ", e);
}
} else {
DefaultGrailsRuntime dgr = new DefaultGrailsRuntime(createExecutionContext(targetName, args));
dgr.run();
}
}
protected AbstractGrailsRuntime.ExecutionContext createExecutionContext(String targetName, String args) throws MojoExecutionException {
final String targetDir = this.project.getBuild().getDirectory();
ForkedGrailsRuntime.ExecutionContext ec = new ForkedGrailsRuntime.ExecutionContext();
ec.setBuildDependencies( resolveBuildDependencies() );
List<File> providedDependencies = resolveArtifacts("provided");
List<File> compileDependencies = resolveArtifacts("compile");
List<File> runtimeDependencies = resolveArtifacts(COMPILE_PLUS_RUNTIME_SCOPE);
Set<File> testDependencies = new HashSet<File>( resolveArtifacts("test") );
testDependencies.addAll( providedDependencies );
testDependencies.addAll( compileDependencies );
testDependencies.addAll(runtimeDependencies);
ec.setProvidedDependencies(providedDependencies);
ec.setRuntimeDependencies(new ArrayList<File>(runtimeDependencies));
ec.setCompileDependencies(compileDependencies);
ec.setTestDependencies(new ArrayList<File>(testDependencies));
ec.setGrailsWorkDir(new File(grailsWorkDir));
ec.setProjectWorkDir(new File(targetDir));
ec.setClassesDir(new File(targetDir, "classes"));
ec.setTestClassesDir(new File(targetDir, "test-classes"));
ec.setResourcesDir(new File(targetDir, "resources"));
ec.setProjectPluginsDir(this.pluginsDir);
ec.setForkedVmArgs(this.forkedVmArgs);
// If the command is running in non-interactive mode, we
// need to pass on the relevant argument.
if (this.nonInteractive) {
args = (args != null) ? "--non-interactive " + args : "--non-interactive ";
}
// If the project has specified to print stacktraces to the console
// turn on the flag in the arguments.
if (this.showStacktrace) {
args = (args != null) ? "--stacktrace " + args : "--stacktrace ";
}
// these two settings are only used if running inside of an ide
if (this.grailsBuildListener != null) {
ec.setGrailsBuildListener(grailsBuildListener);
}
if (this.dependencyFileLocation != null) {
ec.setDependencyFileLocation(new File(dependencyFileLocation));
}
// Enable the plain output for the Grails command to fix an issue with JLine
// consuming the standard output after execution via Maven.
args = (args != null) ? "--plain-output " + args : "--plain-output";
ec.setArgs(args);
ec.setScriptName(targetName);
ec.setBaseDir(project.getBasedir());
ec.setEnv(getEnvironment());
return ec;
}
protected Collection<File> resolveArtifactIds(Collection<String> artifactIds) throws MojoExecutionException {
Collection<ArtifactRequest> requests = new ArrayList<ArtifactRequest>();
for (String artifactId : artifactIds) {
ArtifactRequest request = new ArtifactRequest();
request.setArtifact(
new DefaultArtifact(artifactId));
request.setRepositories( remoteRepos );
getLog().debug("Resolving artifact " + artifactId +
" from " + remoteRepos);
requests.add(request);
}
Collection<File> files = new ArrayList<File>();
try
{
List<ArtifactResult> result = repoSystem.resolveArtifacts(repoSession, requests);
for (ArtifactResult artifactResult : result) {
File file = artifactResult.getArtifact().getFile();
files.add(file);
getLog().debug("Resolved artifact " + artifactResult.getArtifact().getArtifactId() + " to " +
file + " from "
+ artifactResult.getRepository());
}
} catch ( ArtifactResolutionException e ) {
throw new MojoExecutionException( e.getMessage(), e );
}
return files;
}
protected File resolveArtifact(String artifactId) throws MojoExecutionException {
ArtifactRequest request = new ArtifactRequest();
request.setArtifact(
new DefaultArtifact(artifactId));
request.setRepositories( remoteRepos );
getLog().debug("Resolving artifact " + artifactId +
" from " + remoteRepos);
ArtifactResult result;
File file;
try
{
result = repoSystem.resolveArtifact( repoSession, request );
file = result.getArtifact().getFile();
} catch ( ArtifactResolutionException e ) {
throw new MojoExecutionException( e.getMessage(), e );
}
getLog().debug("Resolved artifact " + artifactId + " to " +
file + " from "
+ result.getRepository());
return file;
}
/**
* Resolves artifacts to files including transitive resolution
*
* @return
* @throws MojoExecutionException
*/
protected List<File> resolveArtifacts() throws MojoExecutionException {
return resolveArtifacts( "compile");
}
protected List<File> resolveArtifacts(Collection<String> scopes) throws MojoExecutionException {
MavenProject mavenProject = project;
return resolveArtifacts(mavenProject, scopes);
}
protected List<File> resolveArtifacts(String scope) throws MojoExecutionException {
MavenProject mavenProject = project;
return resolveArtifacts(mavenProject, Arrays.asList(scope));
}
protected List<File> resolveArtifacts(MavenProject mavenProject, Collection<String> scopes) throws MojoExecutionException {
return resolveArtifacts(mavenProject, scopes, null);
}
protected List<File> resolveArtifacts(MavenProject mavenProject, Collection<String> scopes, DependencyFilter filter) throws MojoExecutionException {
try {
DefaultDependencyResolutionRequest request = new DefaultDependencyResolutionRequest(mavenProject, repoSession);
if(filter != null) {
request.setResolutionFilter(new AndDependencyFilter(new ScopeDependencyFilter(scopes, Collections.<String>emptyList()), filter));
}
else {
request.setResolutionFilter(new ScopeDependencyFilter(scopes, Collections.<String>emptyList()));
}
DependencyResolutionResult result = projectDependenciesResolver.resolve(request);
List<org.eclipse.aether.graph.Dependency> dependencies = result.getDependencies();
final List<File> files = new ArrayList<File>();
for(org.eclipse.aether.graph.Dependency d : dependencies) {
org.eclipse.aether.artifact.Artifact artifact = d.getArtifact();
File file = artifact.getFile();
if(file != null) {
String name = file.getName();
if(!name.contains("xml-apis") && !name.contains("commons-logging"))
files.add(file);
}
}
return files;
} catch (DependencyResolutionException e) {
throw new MojoExecutionException("Dependency resolution failure: " + e.getMessage(), e);
}
}
protected List<File> resolveBuildDependencies() throws MojoExecutionException {
try {
/*
* Get the Grails dependencies from the plugin's POM file first.
*/
final MavenProject pluginProject = getPluginProject();
/*
* Add the plugin's dependencies and the project using the plugin's dependencies to the list
* of unresolved dependencies. This is done so they can all be resolved at the same time so
* that we get the benefit of Maven's conflict resolution.
*/
Set<File> jars = new HashSet<File>();
// calculate the Grails version to use from the dependency or grailsVersion setting
String grailsVersion = establishGrailsVersion();
String groovyVersion = establishGroovyVersion();
if(grailsVersion != null) {
String scriptsId = "org.grails:grails-scripts:" + grailsVersion;
String bootstrapId = "org.grails:grails-bootstrap:" + grailsVersion;
String groovyId = "org.codehaus.groovy:groovy-all:" + groovyVersion;
jars.addAll(resolveArtifactIds(Arrays.asList(scriptsId, bootstrapId, groovyId)));
}
jars.addAll(resolveArtifacts(pluginProject, COMPILE_PLUS_RUNTIME_SCOPE, new ExclusionsDependencyFilter(Arrays.asList("org.grails:grails-bootstrap", "org.codehaus.groovy:groovy-all", "org.codehaus.groovy:groovy"))));
findAndAddToolsJar(jars);
addExtraClassPathEntries(jars);
return new ArrayList<File>(jars);
} catch (final Exception e) {
throw new MojoExecutionException("Failed to create classpath for Grails execution.", e);
}
}
protected String establishGroovyVersion() throws ProjectBuildingException {
if(this.groovyVersion == null) {
Artifact groovyDependency = findGroovyDependency(project);
if(groovyDependency != null) {
this.groovyVersion = groovyDependency.getVersion();
}
else {
String grailsVersion = establishGrailsVersion();
if(grailsVersion.startsWith("2.3")) {
this.groovyVersion = "2.1.9";
}
else {
this.groovyVersion = findGroovyVersionFromPlugin();
if(this.groovyVersion == null) {
this.groovyVersion = "2.3.3";
}
}
}
}
return this.groovyVersion;
}
protected String establishGrailsVersion() throws ProjectBuildingException {
if(this.grailsVersion == null) {
Artifact grailsDependency = findGrailsDependency(project);
if(grailsDependency != null) {
this.grailsVersion = grailsDependency.getVersion();
}
else {
this.grailsVersion = findGrailsVersionFromPlugin();
}
}
return this.grailsVersion;
}
private String findGrailsVersionFromPlugin() throws ProjectBuildingException {
return findArtefactVersionFromPlugin("org.grails", "grails-bootstrap");
}
private String findGroovyVersionFromPlugin() throws ProjectBuildingException {
return findArtefactVersionFromPlugin("org.codehaus.groovy", "groovy");
}
private String findArtefactVersionFromPlugin(String group, String name) throws ProjectBuildingException {
MavenProject pluginProject = getPluginProject();
List<Dependency> dependencyArtifacts = pluginProject.getDependencies();
if(dependencyArtifacts != null) {
for (Dependency d : dependencyArtifacts) {
if (d.getArtifactId().equals(name) &&
d.getGroupId().equals(group)) {
return d.getVersion();
}
}
}
return null;
}
private void handleVersionSync() throws MojoExecutionException {
// Search for all Grails plugin dependencies and install
// any that haven't already been installed.
final Properties metadata = new Properties();
File metadataFile = new File(getBasedir(), "application.properties");
FileReader reader = null;
FileWriter writer = null;
try {
boolean created = true;
if(!metadataFile.exists()) {
created = metadataFile.createNewFile();
}
if(created) {
reader = new FileReader(metadataFile);
metadata.load(reader);
boolean metadataModified = syncVersions(metadata);
if (metadataModified) {
writer = new FileWriter(metadataFile);
metadata.store(writer, "Grails Metadata file");
}
}
} catch (IOException e) {
throw new MojoExecutionException("Failed to sync application version with Maven plugin defined version");
} finally {
try {
if(reader != null)
reader.close();
if(writer != null)
writer.close();
} catch (IOException e) {
// ignore
}
}
}
private boolean syncVersions(Properties metadata) {
boolean result = false;
Object grailsVersion = metadata.get(APP_GRAILS_VERSION);
Artifact grailsDependency = findGrailsDependency(project);
if (grailsDependency != null) {
if (!grailsDependency.getVersion().equals(grailsVersion)) {
metadata.put(APP_GRAILS_VERSION, grailsDependency.getVersion());
result = true;
}
}
Object appVersion = metadata.get(APP_VERSION);
if (!project.getVersion().equals(appVersion)) {
metadata.put(APP_VERSION, project.getVersion());
result = true;
}
Object appName = metadata.get(APP_NAME);
if (!project.getArtifactId().equals(appName)) {
metadata.put(APP_NAME, project.getName());
result = true;
}
return result;
}
private Artifact findGrailsDependency(MavenProject project) {
Set dependencyArtifacts = project.getDependencyArtifacts();
for (Object o : dependencyArtifacts) {
Artifact artifact = (Artifact) o;
if (artifact.getArtifactId().equals("grails-dependencies") &&
artifact.getGroupId().equals("org.grails")) {
return artifact;
}
}
return null;
}
private Artifact findGroovyDependency(MavenProject project) {
Set dependencyArtifacts = project.getDependencyArtifacts();
for (Object o : dependencyArtifacts) {
Artifact artifact = (Artifact) o;
String groupId = artifact.getGroupId();
String artifactId = artifact.getArtifactId();
if (groupId.equals("org.codehaus.groovy") &&
(artifactId.equals("groovy-all") || artifactId.equals("groovy"))) {
return artifact;
}
}
return null;
}
private void configureMavenProxy() {
if (settings != null) {
Proxy activeProxy = settings.getActiveProxy();
if (activeProxy != null) {
String host = activeProxy.getHost();
int port = activeProxy.getPort();
String noProxy = activeProxy.getNonProxyHosts();
String username = activeProxy.getUsername();
String password = activeProxy.getPassword();
System.setProperty("http.proxyHost", host);
System.setProperty("http.proxyPort", String.valueOf(port));
if (noProxy != null) {
System.setProperty("http.nonProxyHosts", noProxy);
}
if (username != null) {
System.setProperty("http.proxyUser", username);
}
if (password != null) {
System.setProperty("http.proxyPassword", password);
}
}
}
}
private void addExtraClassPathEntries(Set<File> jars) {
if (extraClasspathEntries != null) {
String[] entriesArr = extraClasspathEntries.split(",");
for (int i = 0; i < entriesArr.length; i++) {
// check for comma
String entry;
if (entriesArr[i].endsWith("\\") && i < entriesArr.length-1) {
entry = entriesArr[i] + "," + entriesArr[++i];
} else {
entry = entriesArr[i];
}
File file = new File(entry);
if (!file.exists()) {
this.getLog().warn("Grails extra classpath entry " + file + " does not exist.");
}
jars.add(file);
}
}
}
/*
* Add the "tools.jar" to the classpath so that the Grails scripts can run native2ascii.
* First assume that "java.home" points to a JRE within a JDK. NOTE that this will not
* provide a valid path on Mac OSX. This is not a big deal, as the JDK on Mac OSX already
* adds the required JAR's to the classpath. This logic is really only for Windows/*Unix.
*/
private void findAndAddToolsJar(Set<File> jars) {
final String javaHome = System.getProperty("java.home");
File toolsJar = new File(javaHome, "../lib/tools.jar");
if (!toolsJar.exists()) {
// The "tools.jar" cannot be found with that path, so
// now try with the assumption that "java.home" points
// to a JDK.
toolsJar = new File(javaHome, "tools.jar");
}
if (toolsJar.exists()) {
if (toolsJar != null) {
jars.add(toolsJar);
}
}
}
private MavenProject getPluginProject() throws ProjectBuildingException {
final Artifact pluginArtifact = findArtifact(this.project.getPluginArtifacts(), "org.grails", "grails-maven-plugin");
DefaultProjectBuildingRequest request = new DefaultProjectBuildingRequest();
request.setLocalRepository(localRepository)
.setRemoteRepositories(project.getRemoteArtifactRepositories())
.setPluginArtifactRepositories(project.getPluginArtifactRepositories())
.setRepositoryMerging(ProjectBuildingRequest.RepositoryMerging.REQUEST_DOMINANT);
return projectBuilder.build(pluginArtifact, request).getProject();
}
/**
* Finds the requested artifact in the supplied artifact collection.
*
* @param artifacts A collection of artifacts.
* @param groupId The group ID of the artifact to be found.
* @param artifactId The artifact ID of the artifact to be found.
* @return The artifact from the collection that matches the group ID and
* artifact ID value or {@code null} if no match is found.
*/
private Artifact findArtifact(final Collection<Artifact> artifacts, final String groupId, final String artifactId) {
for (final Artifact artifact : artifacts) {
if (artifact.getGroupId().equals(groupId) && artifact.getArtifactId().equals(artifactId)) {
return artifact;
}
}
return null;
}
}